import * as shlib from 'shlib/browser'

import { ShLineElement } from './ShLineElement'
import { ShSpanElement } from './ShSpanElement'


export interface ShTextAttribute {
    backColor: string
    foreColor: string
    bold: boolean
    italic: boolean
    underline: boolean
}

export interface ShTermCreateOptions {
    fontFamily: string
    fontSize: number
    lineHeight: number
    backColor: string
    foreColor: string
}

export class ShTerm {

    private _options: ShTermCreateOptions

    private $parent: HTMLElement
    private $window: HTMLElement
    private $content: HTMLElement
    private $caret: HTMLElement   
    private $scrollbar: HTMLElement
    private _resizeObserver: ResizeObserver

    private _font!: shlib.Font
    private _currTextAttr: ShTextAttribute
    private _currWorkMode: 'edit' | 'draw'
    private _currEditMode: 'insert' | 'replace'

    private _cellWidth!: number
    private _cellHeight!: number
    private _rows!: number
    private _cols!: number
    
    private _scrollRow: number
    private _caretRow: number
    private _caretCol: number

    public readonly $el: HTMLElement

    public get options(): ShTermCreateOptions { return this._options }
    public get font(): shlib.Font { return this._font }
    public get currTextAttr(): ShTextAttribute { return this._currTextAttr }
    public get currWorkMode(): 'edit' | 'draw' { return this._currWorkMode }
    public get currEditMode(): 'insert' | 'replace' { return this._currEditMode }
    public get cellWidth(): number { return this._cellWidth }
    public get cellHeight(): number { return this._cellHeight }
    public get rows(): number { return this._rows }
    public get cols(): number { return this._cols }

    constructor(parent: HTMLElement, options: Partial<ShTermCreateOptions>) {
        this.$parent = parent
        this.$parent.innerHTML = `
            <div class="shterm-container">
                <div class="shterm-window" tabindex="0">
                    <div class="shterm-content"></div>
                    <div class="shterm-caret block blink"></div>
                </div>
                <div class="shterm-scrollbar">
                    <div class="shterm-scrollbar-thumb"></div>
                </div>
            </div>
        `

        this.$el = this.$parent.querySelector('.shterm-container')! as HTMLElement
        (this.$el as any).$term = this

        this.$window = this.$parent.querySelector('.shterm-window') as HTMLElement
        this.$window.onkeydown = (e: KeyboardEvent) => this._on_keydown(e)

        this.$content = this.$parent.querySelector('.shterm-content') as HTMLElement
        this.$caret = this.$parent.querySelector('.shterm-caret') as HTMLElement
        this.$scrollbar = this.$parent.querySelector('.shterm-scrollbar') as HTMLElement

        this._options = {
            fontFamily: 'Consolas, "微软雅黑"',
            fontSize: 16,
            lineHeight: 1.2,
            backColor: '#000',
            foreColor: '#fff',
        }

        this._currTextAttr = {
            backColor: '', // 空字符串表示默认背景色
            foreColor: '', // 空字符串表示默认前景色
            bold: false,
            italic: false,
            underline: false,
        }
        this._currWorkMode = 'edit'
        this._currEditMode = 'insert'

        this._scrollRow = 0
        this._caretRow = 0
        this._caretCol = 0

        this._resizeObserver = new ResizeObserver(() => {
            this._update_layout()
        })
        this._resizeObserver.observe(this.$window)


        this.setOptions(options) // 会调用 this._updateLayout()
    }

    public setOptions(options: Partial<ShTermCreateOptions>) {
        this._options = {
            ...this._options,
            ...options,
        }

        this._font = new shlib.Font(this._options.fontFamily, this._options.fontSize)
        if (this._font.monoCharWidth === 0)
            throw new Error(`字体 "${this._font.family}" 不是一个等宽字体。`)
        
        this.$window.updateCssStyle({
            fontFamily: this._font.family,
            fontSize: `${this._font.size}px`,
            backgroundColor: this._options.backColor,
            color: this._options.foreColor,
        })

        this._update_layout()
    }


    public setCaretStyle(shape: 'block' | 'bar' | 'line' | 'hidden', blink?: boolean) {
        this.$caret.toggleClass('block', shape === 'block')
        this.$caret.toggleClass('bar', shape === 'bar')
        this.$caret.toggleClass('line', shape === 'line')
        this.$caret.toggleClass('blink', blink)
    }

    /**
     * 从光标处开始写入文本，使用当前文本属性。
     * 
     * 不会对参数 `text` 中的内容进行转义解释，遇到不可显示的字符直接忽略。调用者应确保其中只包
     * 含可显示字符。
     * 
     * 本方法会把 `text` 拆分为若干个由宽度相等的字符构成段，每个段分别绘制，确保每个字符占据 1 个
     * 或 2 个单元格，从而实现中英文 2:1 严格对齐。
     */
    public writeText(text: string) {
        const that = this

        function *_split_text(text: string): Generator<string> {
            let span_chars: string[] = []
            let span_cw = 0
            for (let i = 0; i < text.length; i++) {
                const c = text[i]!
                if (shlib.Font.charType(c) === 'ctrl')
                    continue

                const cw = that._font.charWidth(c, that._currTextAttr.bold)
                if (cw === span_cw) {
                    // 当前字符宽度和之前的字符宽度相同，继续累积
                    span_chars.push(c)
                }
                else {
                    // 当前字符宽度和之前的字符宽度不同，把已累积的吐出，并重新开始累积
                    if (span_chars.length >= 0)
                        yield span_chars.join('')
                    span_chars = [c,]
                    span_cw = cw
                }
            }

            // 吐出最后一段
            if (span_chars.length > 0)
                yield span_chars.join('')
        }

        let $line = this.$content.lastElementChild as (ShLineElement | null)
        if (! $line) {
            $line = ShLineElement.create(true)
            this.$content.appendChild($line)
        }

        for (const sp of _split_text(text)) {
            const $span = ShSpanElement.create(this, sp)
            $line.appendSpan($span)
        }
    }

    private _update_layout() {
        this._cellWidth = Math.ceil(Math.max(this._font.monoCharWidthBold, this._font.size / 2))
        this._cellHeight = Math.ceil(this._font.size * this._options.lineHeight)
        this._rows = Math.floor(this.$window.clientHeight / this._cellHeight)
        this._cols = Math.floor(this.$window.clientWidth / this._cellWidth)

        const paddingLeft = Math.floor((this.$window.clientWidth - this._cols * this._cellWidth) / 2)
        const paddingTop = Math.floor((this.$window.clientHeight - this._rows * this._cellHeight) / 2)
        this.$window.updateCssStyle({
            padding: `${paddingTop}px ${paddingLeft}px`,
            '--shterm-cell-width': `${this._cellWidth}px`,
            '--shterm-cell-height': `${this._cellHeight}px`,
            '--shterm-diff-width1': `${this._cellWidth - this._font.monoCharWidthBold}px`,
            '--shterm-diff-width2': `${2 * this._cellWidth - this._font.size}px`,
        })

        for (const $line of this.$content.children) {
            ($line as ShLineElement).updateLayout()
        }

        this.$caret.updateCssStyle({
            left: `${paddingLeft + this._caretCol * this._cellWidth}px`,
            top: `${paddingTop + this._caretRow * this._cellHeight}px`,
        })
    }

    private _on_keydown(e: KeyboardEvent) {
        if (e.key === 'ArrowRight') {
            this._caret_move(this._caretRow, this._caretCol + 1)
        }
        else if (e.key === 'ArrowLeft') {
            this._caret_move(this._caretRow, this._caretCol - 1)
        }
        else if (e.key === 'ArrowUp') {
            this._caret_move(this._caretRow - 1, this._caretCol)
        }
        else if (e.key === 'ArrowDown') {
            this._caret_move(this._caretRow + 1, this._caretCol)
        }
    }

    /**
     * 解析字符串，把其中的控制命令抽取出来。
     */
    public _parse_message(msg: string) {
        for (let i = 0; i < msg.length; i++) {
            // 查找 C0 或 C1 控制字符
            const c = msg[i]!
        }
    }

    private _caret_move(row: number, col: number) {
        if (row < 0)
            row = 0
        if (row >= this._rows)
            row = this._rows - 1

        if (col < 0)
            col = 0
        if (col >= this._cols)
            col = this._cols - 1
        
        this.$caret.toggleClass('blink', false)

        const paddingLeft = Math.floor((this.$window.clientWidth - this._cols * this._cellWidth) / 2)
        const paddingTop = Math.floor((this.$window.clientHeight - this._rows * this._cellHeight) / 2)

        this.$caret.updateCssStyle({
            left: `${paddingLeft + col * this._cellWidth}px`,
            top: `${paddingTop + row * this._cellHeight}px`,
        })

        this._caretRow = row
        this._caretCol = col

        this.$caret.toggleClass('blink', true)
    }

    /**
     * 在指定位置，用当前文本属性，插入或覆写入指定文本段。
     * 
     * @param span 要写入的文本段。
     */
    private _write_text_span(row: number, col: number, span: string) {

    }
}


